﻿#region Header
// ------------------------ Licence / Copyright ------------------------
// 
// ScrumTable for Scrum Meeting Support
// Copyright © HSR - Hochschule für Technik Rapperswil
// All Rights Reserved
// 
// Author:
//  Michael Gfeller Silvan Gehrig Patrick Boos
// 
// ---------------------------------------------------------------------
#endregion

#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using Microsoft.TeamFoundation.WorkItemTracking.Client;

using ScrumTable.Common;
using ScrumTable.Common.Collections;
using ScrumTable.Common.FileSystem;
using ScrumTable.Common.Logging;
using ScrumTable.Common.Security;
using ScrumTable.Common.Text;
using ScrumTable.DL.Data.Generic.Cache;
using ScrumTable.DL.Data.Schema;
using ScrumTable.DL.Data.TFS.Bulk.Properties;
using ScrumTable.DL.Data.TFS.Bulk.Services.IterationHandling;
using ScrumTable.DL.Data.TFS.Bulk.Services.Utils;
using ScrumTable.DL.Data.TFS.Services.TeamFoundation;

using WorkItemBucket = ScrumTable.DL.Data.Generic.Cache.CacheElementListEntryBucket<ScrumTable.DL.Data.Generic.Cache.CacheWorkItem>;
using IterationBucket = ScrumTable.DL.Data.Generic.Cache.CacheElementListEntryBucket<ScrumTable.DL.Data.Generic.Cache.CacheIteration>;

using WorkItemSavePair = ScrumTable.DL.Data.Generic.Cache.CacheSavePair<ScrumTable.DL.Data.Generic.Cache.CacheElementListEntryBucket<ScrumTable.DL.Data.Generic.Cache.CacheWorkItem>>;
using IterationSavePair = ScrumTable.DL.Data.Generic.Cache.CacheSavePair<ScrumTable.DL.Data.Generic.Cache.CacheElementListEntryBucket<ScrumTable.DL.Data.Generic.Cache.CacheIteration>>;

#endregion

namespace ScrumTable.DL.Data.TFS.Bulk.Services
{
    /// <summary>
    /// This class is used to synchronize data from the data layer to the
    /// TFS data storage.
    /// </summary>
    internal class DataSaveProcess
    {
        #region Declarations
        //--------------------------------------------------------------------
        // Declarations
        //--------------------------------------------------------------------

        private readonly CacheDataChanges _changes;
        private readonly CacheIdentityStore _identities;
        private readonly ConnectionInformationManager _connectionCtx;
        private readonly Uri _backEndServerUri;
        private readonly WorkItemDataLoader _workItemDataImporter;

        #endregion

        #region Properties
        //--------------------------------------------------------------------
        // Properties
        //--------------------------------------------------------------------

        #endregion

        #region Constructors / Destructor
        //--------------------------------------------------------------------
        // Constructors / Destructor
        //--------------------------------------------------------------------

        /// <summary>
        /// Creates a new DataSaveProcess instance, no public constructor
        /// needed.
        /// </summary>
        /// <param name="connectionCtx">Specifies the connection information which is required to connect to the tfs.</param>
        /// <param name="backEndServerUri">Specifies the url of the back end system (tfs).</param>
        /// <param name="changes">Specifies the change set to synchronize.</param>
        internal DataSaveProcess(ConnectionInformationManager connectionCtx, Uri backEndServerUri, CacheDataChanges changes)
        {
            PreCondition.AssertNotNull(connectionCtx, "connectionCtx");
            PreCondition.AssertNotNull(backEndServerUri, "backEndServerUri");
            PreCondition.AssertNotNull(changes, "changes");

            _connectionCtx = connectionCtx;
            _backEndServerUri = backEndServerUri;
            _changes = changes;
            _identities = changes.Identities.ToWriteable();
            _workItemDataImporter = new WorkItemDataLoader(new CacheIdentityElementFactory(
                _identities,
                changes.IdentitiesIdGenerator));
        }

        #endregion

        #region Methods
        //--------------------------------------------------------------------
        // Methods
        //--------------------------------------------------------------------

        /// <summary>
        /// Synchronizes the changes with the team foundation server and
        /// sets the accepted flag if the change has been successfully
        /// stored back to the back end system.
        /// </summary>
        internal void SyncChanges()
        {
            // establish connection
            using (TfsServerWrapper tfsContext = new TfsConnector(_connectionCtx.Selected, _backEndServerUri).Connect())
            {
                if (!tfsContext.EnsureConnected())
                    throw new InvalidOperationException(Resources.ExcCacheConnectionUnavailable);

                TrySyncIterations(tfsContext);
                TrySyncWorkItems(tfsContext);
            }
        }

        //--------------------------------------------------------------------
        // Sync Iterations
        //--------------------------------------------------------------------

        private void TrySyncIterations(TfsServerWrapper tfsContext)
        {
            try
            {
                SyncIterations(tfsContext);
            }
            catch (Exception e)
            {
                _changes.MarkChangesAsError(e);
                Logger<DataSaveProcess>.Error(Resources.ExcDataSaveProcItersSaveFailed, e);
            }
        }

        private void SyncIterations(TfsServerWrapper tfsContext)
        {
            if (_changes.ChangedIterations.Count > 0)
            {
                TryStoreIterations(tfsContext);
            }
        }

        private void TryStoreIterations(TfsServerWrapper tfsContext)
        {
            IterationSaverBase saver = IterationHandlingFactory.CreateIterationSaver(_changes.Project.OwnerContext.Schema.IterationSchema);
            saver.SaveIterationData(tfsContext, _changes);
        }


        //--------------------------------------------------------------------
        // Sync WorkItems
        //--------------------------------------------------------------------

        private void TrySyncWorkItems(TfsServerWrapper tfsContext)
        {
            try
            {
                SyncWorkItems(tfsContext);
            }
            catch (Exception e)
            {
                _changes.MarkChangesAsError(e);
                Logger<DataSaveProcess>.Error(Resources.ExcDataSaveProcWorkItemsSaveFailed, e);
            }
        }

        private void SyncWorkItems(TfsServerWrapper tfsContext)
        {
            IDictionary<int, WorkItemSaveContext> loadedSaveContexts = null;

            if (_changes.ChangedWorkItems.Count > 0)
            {
                loadedSaveContexts = StoreWorkItemChanges(tfsContext.WorkItems);
            }

            SyncWorkItemsBackFromTfs(tfsContext.WorkItems, loadedSaveContexts);
            SyncUniqueIdsIntoTree();
            ReportErrorsToSavePairs(loadedSaveContexts);
        }

        private IDictionary<int, WorkItemSaveContext> StoreWorkItemChanges(WorkItemStore store)
        {
            // Contains ALL loaded save contexts.
            IDictionary<int, WorkItemSaveContext> loadedSaveContexts = new Dictionary<int, WorkItemSaveContext>();

            if (CreateNewTfsWorkItems(store, loadedSaveContexts))
            {
                RetrieveWorkItemsFromTfs(store, loadedSaveContexts);
                    
                // Contains the work items to save and to accept. If an error occurs during the
                // save process the corresponding work item will be removed from this dictionary.
                // This means, that the "IsSaved" flag wont be set within the SetSavedFlag()
                // procedure.
                IDictionary<int, WorkItemSaveContext> savedWorkItems = new Dictionary<int, WorkItemSaveContext>(loadedSaveContexts);

                UpdateTfsWorkItems(store, savedWorkItems);
                SetSavedFlag(savedWorkItems.Values);
            }
            return loadedSaveContexts;
        }

        /// <summary>
        /// If a work item is stored in cache (generic data layer) but cannot be found on server side, its not
        /// retrieved by this procedure. The work item wont be stored into the <paramref name="localWorkItemStore"/>
        /// dictionary and ignored.
        /// </summary>
        private void RetrieveWorkItemsFromTfs(WorkItemStore store, IDictionary<int, WorkItemSaveContext> localWorkItemStore)
        {
            IDictionary<int, WorkItemBucket> toRetrieve = new Dictionary<int, WorkItemBucket>();

            _changes.ChangedWorkItems.ForEach(
                wi =>
                {
                    if (wi.Value.Element.Id != CacheIdentity.DefaultId && !localWorkItemStore.ContainsKey(wi.Value.Element.Id))
                    {
                        toRetrieve.Add(wi.Value.Element.Id, wi.Value);
                    }
                });

            foreach (WorkItem changedWorkItem in GetWorkItemsFromTfs(store, toRetrieve.Keys.ToArray()))
            {
                localWorkItemStore[changedWorkItem.Id] = new WorkItemSaveContext(changedWorkItem, toRetrieve[changedWorkItem.Id]);
            }
        }

        private void UpdateTfsWorkItems(WorkItemStore store, IDictionary<int, WorkItemSaveContext> localWorkItemStore)
        {
            IList<TfsAttachment> attachmentsToDispose = new List<TfsAttachment>();

            try
            {
                UpdateTfsWorkItems(store, localWorkItemStore, attachmentsToDispose);
                UpdateTfsAttachmentIds(localWorkItemStore);
            }
            finally
            {
                DeleteAttachments(attachmentsToDispose);
            }
        }

        private void UpdateTfsAttachmentIds(IDictionary<int, WorkItemSaveContext> localWorkItemStore)
        {
            foreach (KeyValuePair<int, WorkItemSaveContext> saveContext in localWorkItemStore)
            {
                TfsAttachmentHandler attachments = new TfsAttachmentHandler(saveContext.Value.WorkItem, _connectionCtx.Selected);

                foreach (TfsAttachment tfsAttachment in attachments)
                {
                    // the local file system contains the unique upload ID as parent folder name because the file
                    // name is specified by the user.
                    // the original file from the Business Layer (CacheAttachment) contains the unique upload ID
                    // as file name.

                    // try to retrieve the uploaded attachment by the local file system reference [Attachment REF]
                    string serverRefToLocalPath = tfsAttachment.LocalPath;
                    string serverRefToLocalFileSystem = (serverRefToLocalPath != null)
                        ? Path.GetFileName(Path.GetDirectoryName(serverRefToLocalPath))
                        : string.Empty;

                    // Replace old (or unkown) tfs attachment id with the new attachment id.
                    if (!string.IsNullOrEmpty(serverRefToLocalFileSystem))
                    {
                        CacheAttachment foundAtta = saveContext.Value.CacheBucket.Element.Attachments.TryGetBy(
                            ca =>
                                {
                                    return (Path.GetFileNameWithoutExtension(ca.Uri.ToLocalPath()) == serverRefToLocalFileSystem);
                                });

                        if (foundAtta != null)
                        {
                            foundAtta.AssignId(tfsAttachment.Id);
                            
                            _identities.SetByType<CacheAttachment>(
                                foundAtta.UniqueId,
                                new CacheIdentity(foundAtta.UniqueId, foundAtta.Category, tfsAttachment.Id));
                        }
                    }
                }
            }
        }

        private void UpdateTfsWorkItems(WorkItemStore store, IDictionary<int, WorkItemSaveContext> localWorkItemStore, IList<TfsAttachment> attachmentsToDispose)
        {
            localWorkItemStore.ForEach(
                wiscPair => SaveWorkItem(wiscPair.Value, attachmentsToDispose));

            store.TrySave(
                localWorkItemStore.Values.Convert(wisc => wisc.WorkItem),
                (exc, wi, fields) => HandleSaveError(exc, wi.Id, fields, localWorkItemStore));
        }

        private void ReportErrorsToSavePairs(IDictionary<int, WorkItemSaveContext> loadedSaveContexts)
        {
            if (loadedSaveContexts == null) { return; }

            foreach (var saveContextPair in loadedSaveContexts)
            {
                // do not publish contexts which don't contain errors
                if (!saveContextPair.Value.HasError) { continue; }

                // the errors can only be published if a corresponding
                // Sync element is available
                if (!_changes.WorkItemsToSync.ContainsKey(saveContextPair.Value.CacheBucket.Element.UniqueId)) { continue; }

                // the element couldn't be saved back to the back end system.
                // let's report the error on the layers above
                _changes.WorkItemsToSync[saveContextPair.Value.CacheBucket.Element.UniqueId].Errors.Add(saveContextPair.Value.Errors);
            }
        }

        private void HandleSaveError(Exception error, int workItemId, ICollection<Field> invalidFields, IDictionary<int, WorkItemSaveContext> localWorkItemStore)
        {
            if (localWorkItemStore.ContainsKey(workItemId))
            {
                // retrieve save context
                WorkItemSaveContext saveContext = localWorkItemStore[workItemId];

                // remove work item from list with successfull elements
                localWorkItemStore.Remove(workItemId);

                HandleSaveError(error, invalidFields, saveContext);
            }
        }

        private void HandleSaveError(Exception error, ICollection<Field> invalidFields, WorkItemSaveContext saveContext)
        {
            // dump error trace
            saveContext.SaveTrace.DumpSaveErrorData(invalidFields);

            // make sure errors are stored into the WorkItemSaveContext in order to report them to the layers above
            saveContext.Errors.Add(
                invalidFields,
                field => new CacheSaveError(
                    error,
                    field.Name,
                    field.ReferenceName,
                    field.OriginalValue,
                    field.Value,
                    ErrorReasonEnumConverter.Convert(field.Status)));

            // mark save context as error affected
            saveContext.HasError = true;
        }

        private void SetSavedFlag(IEnumerable<WorkItemSaveContext> localWorkItemStore)
        {
            foreach (var wisc in localWorkItemStore)
            {
                _changes.WorkItemsToSync[wisc.CacheBucket.Element.UniqueId].State = CacheSavePairState.Saved;
            }
        }


        //--------------------------------------------------------------------
        // Save work items procedures
        //--------------------------------------------------------------------

        private void SaveWorkItem(WorkItemSaveContext changedWorkItem, IList<TfsAttachment> attachments)
        {
            // FIRST: Save state in order to update the workflow and change the field states.
            SaveValue(changedWorkItem, changedWorkItem.CacheBucket.Element.Type.StateField.Schema);

            // SECOND: Upload links in order to reassign the elements in the TFS object tree.
            TfsLinkHandler linkHandler = new TfsLinkHandler(changedWorkItem.WorkItem);
            SaveChildRelations(linkHandler, changedWorkItem);
            SaveParentRelations(linkHandler, changedWorkItem);

            // THIRD: Save settings values into the tfs history field.
            SaveSettings(changedWorkItem);

            // FORTH: Save values back into the tfs system. The values must be written AFTER
            //        the workflow changed the tfs field states.
            SaveIterationValue(changedWorkItem);
            SaveValues(changedWorkItem);

            if (attachments != null)
            {
                SaveAttachments(changedWorkItem, attachments);
            }
        }

        private void SaveChildRelations(TfsLinkHandler destination, WorkItemSaveContext source)
        {
            foreach (var changedBucket in source.CacheBucket.Element.ChildLinks.ChangedBuckets)
            {
                switch (changedBucket.State)
                {
                    case CacheElementListEntryState.Removed:
                        destination.RemoveChildLink(changedBucket.Element.RelatedId);
                        break;
                    case CacheElementListEntryState.New:
                        destination.AddChildLink(changedBucket.Element.RelatedId);
                        break;
                }
            }
        }

        private void SaveParentRelations(TfsLinkHandler destination, WorkItemSaveContext source)
        {
            foreach (var changedBucket in source.CacheBucket.Element.ParentLinks.ChangedBuckets)
            {
                switch (changedBucket.State)
                {
                    case CacheElementListEntryState.Removed:
                        destination.RemoveParentLink(changedBucket.Element.RelatedId);
                        break;
                    case CacheElementListEntryState.New:
                        destination.AddParentLink(changedBucket.Element.RelatedId);
                        break;
                }
            }
        }

        private void SaveInitialValues(WorkItemSaveContext source)
        {
            SaveIterationValue(source);

            source.WorkItem.TrySetValue(
                source.CacheBucket.Element.Type.NameField.Schema,
                source.CacheBucket.Element.Name);
        }

        private void SaveSettings(WorkItemSaveContext source)
        {
            string settings = new IniParser().ToString(
                SaveSettingsToString(source.CacheBucket.Element.ChangedSettings),
                ValueParser.Default.ToString);

            source.WorkItem.History = Resources.DatSvProcSaveHistoryBegin + settings + Resources.DatSvProcSaveHistoryEnd;
        }

        private IDictionary<string, ICollection<string>> SaveSettingsToString(IDictionary<string, ICollection<object>> input)
        {
            return input.Convert(valuePair =>
                                 new KeyValuePair<string, ICollection<string>>(
                                     valuePair.Key,
                                     valuePair.Value.Convert(data => ObjectUtil.ConvertToString(data))));
        }

        private void SaveValues(WorkItemSaveContext source)
        {
            // the names of the ICacheTrackedDictionaryBucket correspond to the BackEndId of the CacheFieldDefinitions -> see InitialDataCacheLoader.LoadValues()
            foreach (ICacheTrackedDictionaryBucket changedValueBucket in source.CacheBucket.Element.ChangedValues)
            {
                SaveValue(source, changedValueBucket);
            }
        }

        private void SaveValue(WorkItemSaveContext source, SchemaBase targetSchemaElementToSave)
        {
            // the names of the ICacheTrackedDictionaryBucket correspond to the BackEndId of the CacheFieldDefinitions -> see InitialDataCacheLoader.LoadValues()
            foreach (ICacheTrackedDictionaryBucket changedValueBucket in source.CacheBucket.Element.ChangedValues)
            {
                SaveValue(source, changedValueBucket, targetSchemaElementToSave);
            }
        }

        private void SaveValue(WorkItemSaveContext source, ICacheTrackedDictionaryBucket changedValueBucket, SchemaBase targetSchemaElementToSave = null)
        {
            CacheFieldDefinition fieldDef = source.CacheBucket.Element.Type.TryGetFieldByBackEndId(changedValueBucket.Name);

            if (fieldDef == null || fieldDef.IsSchemaInjected)
                return; // the given field definition must be present in the cache

            if (targetSchemaElementToSave != null && fieldDef.Schema != targetSchemaElementToSave)
                return; // the given schema must correspond to the current field definition

            source.WorkItem.TrySetValue(
                fieldDef.Schema,
                changedValueBucket.Value);
        }

        private void SaveAttachments(WorkItemSaveContext source, IList<TfsAttachment> attachments)
        {
            TfsAttachmentHandler attaHandler = new TfsAttachmentHandler(source.WorkItem, _connectionCtx.Selected);

            foreach (CacheAttachment cacheAtta in source.CacheBucket.Element.Attachments)
            {
                if (cacheAtta.IsDirty)
                {
                    // the following procedures are for uploading the file to TFS. in order
                    // to retrieve the ID of the uploaded file, the functionality specified under [Attachment REF]
                    // is needed.

                    // copy attachment to new temp file location (for upload with the specified file name)
                    FilePoolHandle fileHandle = FilePool.Default.CreateInTempFrom(
                        cacheAtta.Uri,
                        Path.GetFileNameWithoutExtension(cacheAtta.Uri.ToLocalPath()),
                        cacheAtta.Name);

                    // replace current attachment
                    TfsAttachment attaReplacement = attaHandler.ReplaceAttachment(
                        cacheAtta.Id,
                        Resources.DataSvProcAttaReplaceComment,
                        fileHandle.FullPath);

                    // associate file to tfs-attachment (so TfsAttachment.Dispose() will dispose the file-handle)
                    attaReplacement.PropertyData[cacheAtta.Name] = fileHandle;

                    // add the new attachment to the list of attachments to dispose
                    attachments.Add(attaReplacement);
                }
            }
        }

        private void SaveIterationValue(WorkItemSaveContext source)
        {
            source.WorkItem.IterationId = (source.CacheBucket.Element.IterationId == CacheIdentity.DefaultId)
                                              ? source.WorkItem.Project.Id
                                              : source.CacheBucket.Element.IterationId;
        }

        private void DeleteAttachments(IList<TfsAttachment> attachments)
        {
            // delete local stored attachments
            foreach (TfsAttachment attachment in attachments)
            {
                attachment.Dispose();
            }
        }

        private IList<WorkItem> GetWorkItemsFromTfs(WorkItemStore store, IEnumerable<int> idsToRetrieve)
        {
            WiqlQueryBuilder idConditions = new WiqlQueryBuilder();
            idConditions.AppendIdOrConditions(idsToRetrieve);

            if (idConditions.Length > 0)
            {
                string query = string.Format(
                    Resources.QueryWorkItemsById,
                    _changes.Project.UserStoryType.Schema.SchemaMappedName,
                    _changes.Project.TaskType.Schema.SchemaMappedName,
                    _changes.Project.BugType.Schema.SchemaMappedName,
                    "{0}");

                WiqlQueryBuilder workItemQuery = new WiqlQueryBuilder(query, idConditions.ToString());
                IDictionary parameters = new Hashtable
                                             {
                                                 {"project", _changes.Project.Name}
                                             };

                return store.QueryWorkItems(workItemQuery.ToString(), parameters);
            }
            return new List<WorkItem>(0);
        }


        //--------------------------------------------------------------------
        // Sync work items procedures
        //--------------------------------------------------------------------

        /// <summary>
        /// This procedure reestablishes the referenced unique id's in the
        /// child links tree.
        /// </summary>
        private void SyncUniqueIdsIntoTree()
        {
            foreach (KeyValuePair<string, WorkItemSavePair> savePairToSync in _changes.WorkItemsToSync)
            {
                CacheWorkItem toEstablishRelations = savePairToSync.Value.Bucket.Element;

                // fill child element unqiue id's
                foreach (CacheWorkItemLink cacheWorkItemLink in toEstablishRelations.ChildLinks)
                {
                    if (_identities.ContainsKey<CacheWorkItem>(cacheWorkItemLink.RelatedId))
                    {
                        cacheWorkItemLink.RelatedUnqiueId =
                            _identities.GetByType<CacheWorkItem>(cacheWorkItemLink.RelatedId).UniqueId;
                    }
                }

                // fill parent element unqiue id
                CacheWorkItemLink parentLink = toEstablishRelations.ParentLink;

                if (parentLink != null && _identities.ContainsKey<CacheWorkItem>(parentLink.RelatedId))
                {
                    toEstablishRelations.ParentLink.RelatedUnqiueId = _identities.GetByType<CacheWorkItem>(parentLink.RelatedId).UniqueId;
                }
            }
        }

        private void SyncWorkItemsBackFromTfs(WorkItemStore store, IDictionary<int, WorkItemSaveContext> loadedSaveContexts)
        {
            IDictionary<int, WorkItem> allServerSideWorkItems = GetAllWorkItemsFromTfs(store);

            // get back changed work items from TFS
            foreach (KeyValuePair<int, WorkItem> srvWorkItem in allServerSideWorkItems)
            {
                // make sure that work item type is available
                CacheWorkItemType type = _changes.Project.WorkItemTypes.TryGetByName(srvWorkItem.Value.Type.Name);

                if (type == null)
                {
                    continue;
                }

                // make sure that identity revision has changed in meantime
                if (_identities.ContainsKey<CacheWorkItem>(srvWorkItem.Key)
                    && srvWorkItem.Value.Revision == _identities.GetByType<CacheWorkItem>(srvWorkItem.Key).Revision)
                {
                    continue;
                }

                // remember last update date time
                if (srvWorkItem.Value.ChangedDate > _changes.LastSaveTime)
                {
                    _changes.LastSaveTime = srvWorkItem.Value.ChangedDate;
                }

                // sync workitem data
                SyncWorkItemBackFromTfs(store, srvWorkItem, type, loadedSaveContexts);
            }

            // make sure that deleted work items are marked as "Deleted".
            foreach (ICacheIdentity cacheIdentity in _identities.GetAllByType<CacheWorkItem>())
            {
                if (cacheIdentity.Id != CacheIdentity.DefaultId // a corresponding server-side work item must be available
                    && !allServerSideWorkItems.ContainsKey(cacheIdentity.Id)) // the server-side work item cannot be found
                {
                    _changes.WorkItemsToSync[cacheIdentity.UniqueId].State = CacheSavePairState.Deleted;
                }
            }
        }

        private void SyncWorkItemBackFromTfs(WorkItemStore store, KeyValuePair<int, WorkItem> srvWorkItem, CacheWorkItemType type, IDictionary<int, WorkItemSaveContext> loadedSaveContexts)
        {
            WorkItemSavePair newSavePair = null;
            
            if (loadedSaveContexts != null && loadedSaveContexts.ContainsKey(srvWorkItem.Key))
            {
                WorkItemSaveContext saveContext = loadedSaveContexts[srvWorkItem.Key];
                
                // a workitem may have been saved, but there were errors during
                // the save procedure
                if (!saveContext.HasError)
                {
                    // if the element (WorkItem) has already been loaded,
                    // use the version from local save context storage
                    CacheWorkItem newWorkItem = _workItemDataImporter.LoadWorkItem(saveContext.WorkItem, type);
                    CacheSavePairState newWorkItemSyncState = CacheSavePairState.Updated;

                    if (_changes.WorkItemsToSync.ContainsKey(newWorkItem.UniqueId))
                    {
                        // if the work item is registred in the sync dictionary, it
                        // has been saved back to tfs. in this case, its required to
                        // use that sync back state
                        newWorkItemSyncState = _changes.WorkItemsToSync[newWorkItem.UniqueId].State;
                    }

                    // use new states/revision manager for new bucket. this
                    // is imported because only error buckets should accommodate
                    // the same revision as in the change-export provided
                    WorkItemBucket newBucket = new WorkItemBucket { Element = newWorkItem };
                    newSavePair = new WorkItemSavePair(newBucket) { State = newWorkItemSyncState };
                }
            }
            else
            {
                // it's a new server side (not in save progress involved) work item
                WorkItemBucket newBucket = new WorkItemBucket { Element = _workItemDataImporter.LoadWorkItemById(store, srvWorkItem.Key, type) };
                newSavePair = new WorkItemSavePair(newBucket) { State = CacheSavePairState.Updated };
            }

            if (newSavePair != null)
            {
                // create new save pair which is used to sync back into the generic data layer
                _changes.WorkItemsToSync[newSavePair.Bucket.Element.UniqueId] = newSavePair;
                _identities.SetByType<CacheWorkItem>(
                    newSavePair.Bucket.Element.UniqueId,
                    new CacheIdentity(
                        newSavePair.Bucket.Element.UniqueId,
                        newSavePair.Bucket.Element.Category,
                        newSavePair.Bucket.Element.Id,
                        newSavePair.Bucket.Element.Revision));
            }
        }

        private IDictionary<int, WorkItem> GetAllWorkItemsFromTfs(WorkItemStore store)
        {
            IDictionary<int, WorkItem> workItems = new Dictionary<int, WorkItem>();
            IDictionary parameters = new Hashtable
                                         {
                                             {"project", _changes.Project.Name}
                                         };
            string query = string.Format(
                Resources.QueryAllWorkItems,
                _changes.Project.UserStoryType.Schema.SchemaMappedName,
                _changes.Project.TaskType.Schema.SchemaMappedName,
                _changes.Project.BugType.Schema.SchemaMappedName);

            foreach (WorkItem wi in store.Query(query, parameters))
            {
                workItems[wi.Id] = wi;
            }
            return workItems;
        }

        //--------------------------------------------------------------------
        // Create work items procedures
        //--------------------------------------------------------------------

        private bool CreateNewTfsWorkItems(WorkItemStore store, IDictionary<int, WorkItemSaveContext> localWorkItemStore)
        {
            // evaluate new workitems
            IDictionary<string, WorkItemSaveContext> newWorkItems = new Dictionary<string, WorkItemSaveContext>();

            foreach (var workItemBucketPair in _changes.ChangedWorkItems)
            {
                if (workItemBucketPair.Value.State == CacheElementListEntryState.New
                    && !workItemBucketPair.Value.Element.HasId)
                {
                    Project currentProject = store.Projects[_changes.Project.Name];
                    WorkItemType currentWorkItemType = currentProject.WorkItemTypes[workItemBucketPair.Value.Element.Type.BackEndId];
                    WorkItem newWorkItem = new WorkItem(currentWorkItemType);

                    WorkItemSaveContext saveContext = new WorkItemSaveContext(newWorkItem, workItemBucketPair.Value);
                    newWorkItems.Add(workItemBucketPair.Key.Top, saveContext);
                    SaveInitialValues(saveContext);
                }
            }

            if (TrySaveNew(store, newWorkItems.Values))
            {
                FillLocalWorkItemStore(newWorkItems, localWorkItemStore);
                FillIdsIntoCacheTree(newWorkItems);
                FillIdsIntoIdentityStore(newWorkItems);
                return true;
            }
            else
            {
                // something went completely wrong. we have to rollback the workitem creation
                UndoTrySaveNew(
                    store,
                    from newWorkItem in newWorkItems
                        where newWorkItem.Value.WorkItem.Id != TfsWorkItem.DefaultId
                        select newWorkItem.Value.WorkItem.Id);
                return false;
            }
        }

        private void UndoTrySaveNew(WorkItemStore store, IEnumerable<int> workItemsToSave)
        {
            try
            {
                Logger<DataSaveProcess>.Warn(Resources.LogDataSaveProcCreateUndo);

                if (workItemsToSave.Count() > 0)
                {
                    store.DestroyWorkItems(workItemsToSave).ForEach(
                        errorInfo =>
                        {
                            Logger<DataSaveProcess>.Error(Resources.LogDataSaveProcCreateUndo, errorInfo.Exception);
                        });
                }
            }
            catch (Exception e)
            {
                Logger<DataSaveProcess>.Error(Resources.LogDataSaveProcCreateUndo, e);
            }
        }

        private void FillLocalWorkItemStore(IDictionary<string, WorkItemSaveContext> newWorkItems, IDictionary<int, WorkItemSaveContext> localWorkItemStore)
        {
            foreach (KeyValuePair<string, WorkItemSaveContext> newWorkItem in newWorkItems)
            {
                localWorkItemStore[newWorkItem.Value.WorkItem.Id] = newWorkItem.Value;
            }
        }

        private void FillIdsIntoCacheTree(IDictionary<string, WorkItemSaveContext> newWorkItems)
        {
            // update all references (link id's) in the work item store (loop through all ChangedWorkItems is important)
            foreach (KeyValuePair<ValueChain<string>, WorkItemBucket> cacheSavePair in _changes.ChangedWorkItems)
            {
                if (newWorkItems.ContainsKey(cacheSavePair.Value.Element.UniqueId))
                {
                    cacheSavePair.Value.Element.AssignId(newWorkItems[cacheSavePair.Value.Element.UniqueId].WorkItem.Id);
                }

                FillIdsIntoCacheLinkTree(newWorkItems, cacheSavePair.Value.Element.ChildLinks);
                FillIdsIntoCacheLinkTree(newWorkItems, cacheSavePair.Value.Element.ParentLinks);
            }
        }

        private void FillIdsIntoCacheLinkTree(IDictionary<string, WorkItemSaveContext> newWorkItems, CacheElementList<CacheWorkItemLink> relationLinks)
        {
            foreach (CacheWorkItemLink link in relationLinks)
            {
                if (newWorkItems.ContainsKey(link.RelatedUnqiueId))
                {
                    link.RelatedId = newWorkItems[link.RelatedUnqiueId].WorkItem.Id;
                }
            }
        }

        private void FillIdsIntoIdentityStore(IDictionary<string, WorkItemSaveContext> newWorkItems)
        {
            foreach (KeyValuePair<string, WorkItemSaveContext> newWorkItem in newWorkItems)
            {
                _identities.SetByType<CacheWorkItem>(
                    newWorkItem.Key,
                    new CacheIdentity(
                        newWorkItem.Value.CacheBucket.Element.UniqueId,
                        newWorkItem.Value.CacheBucket.Element.Category,
                        newWorkItem.Value.CacheBucket.Element.Id,
                        newWorkItem.Value.CacheBucket.Element.Revision));
            }
        }

        private bool TrySaveNew(WorkItemStore store, IEnumerable<WorkItemSaveContext> workItemsToSave)
        {
            bool hasError = false;
            IDictionary<WorkItem, WorkItemSaveContext> workItemIndex = workItemsToSave.ToDictionary(wisc => wisc.WorkItem);

            store.TrySave(
                workItemIndex.Keys,
                (exc, wi, field) =>
                    {
                        HandleSaveError(exc, field, workItemIndex[wi]);
                        hasError = true;
                    });

            return !hasError;
        }

        #endregion

        #region Events
        //--------------------------------------------------------------------
        // Events
        //--------------------------------------------------------------------

        #endregion



        //--------------------------------------------------------------------
        // Nested Classes
        //--------------------------------------------------------------------

        private class WorkItemSaveContext
        {
            #region Declarations
            //--------------------------------------------------------------------
            // Declarations
            //--------------------------------------------------------------------
            
            private bool _hasErrors;

#endregion

            #region Properties
            //--------------------------------------------------------------------
            // Properties
            //--------------------------------------------------------------------

            internal WorkItem WorkItem { get; private set; }
            internal WorkItemBucket CacheBucket { get; private set; }
            internal SaveTraceErrorProvider SaveTrace { get; private set; }
            internal IList<CacheSaveError> Errors { get; private set; }
            internal bool HasError
            {
                get { return (Errors.Count > 0) ? true : _hasErrors; }
                set { _hasErrors = value; }
            }
            
            #endregion

            #region Constructors / Destructor
            //--------------------------------------------------------------------
            // Constructors / Destructor
            //--------------------------------------------------------------------

            internal WorkItemSaveContext(
                WorkItem workItem,
                WorkItemBucket bucket)
            {
                PreCondition.AssertNotNull(workItem, "workItem");
                PreCondition.AssertNotNull(bucket, "bucket");

                WorkItem = workItem;
                CacheBucket = bucket;
                Errors = new List<CacheSaveError>();
                SaveTrace = new SaveTraceErrorProvider(workItem, bucket.Element);
            }

            #endregion
        }
    }
}

